{
 "cells": [
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# Fairness auditing for subgroups using Fairness Aware Counterfactuals for Subgroups (FACTS).\n",
    "\n",
    "[FACTS](https://arxiv.org/abs/2306.14978) is an efficient, model-agnostic, highly parameterizable, and explainable framework for auditing subgroup fairness through counterfactual explanations. FACTS focuses on identifying a specific type of bias, i.e. the *difficulty in achieving recourse*. In short, it focuses on the population that has obtained the unfavorable outcome (*affected population*) by a ML model and tries to identify differences in the difficulty of changing the ML model's decision to obtain the favorable outcome, between affected subpopulations.\n",
    "\n",
    "In this notebook, we will see how to use this algorithm for discovering subgroups where the bias of a model (logistic regression for simplicity) between Males and Females is high.\n",
    "\n",
    "We will use the Adult dataset from UCI ([reference](https://archive.ics.uci.edu/ml/datasets/adult))."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# Preliminaries"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Import dependencies\n",
    "\n",
    "As usual in python, the first step is to import all necessary packages."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 1,
   "metadata": {},
   "outputs": [
    {
     "name": "stderr",
     "output_type": "stream",
     "text": [
      "WARNING:root:No module named 'tempeh': fetch_lawschool_gpa will be unavailable. To install, run:\n",
      "pip install 'aif360[LawSchoolGPA]'\n"
     ]
    }
   ],
   "source": [
    "from sklearn.model_selection import train_test_split\n",
    "from sklearn.linear_model import LogisticRegression\n",
    "from sklearn.compose import ColumnTransformer\n",
    "from sklearn.pipeline import Pipeline\n",
    "from sklearn.preprocessing import OneHotEncoder\n",
    "\n",
    "from aif360.sklearn.datasets.openml_datasets import fetch_adult\n",
    "from aif360.sklearn.detectors.facts.clean import clean_dataset\n",
    "from aif360.sklearn.detectors.facts import FACTS, FACTS_bias_scan\n",
    "\n",
    "from IPython.display import display\n",
    "\n",
    "import warnings\n",
    "warnings.filterwarnings(\"ignore\")"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Below, you can change the `random_seed` variable to `None` if you would like for the pseudo-random parts to actually change between runs. We have set it to a specific value for reproducibility."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 2,
   "metadata": {},
   "outputs": [],
   "source": [
    "random_seed = 131313 # for reproducibility"
   ]
  },
  {
   "attachments": {},
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Load Dataset"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 3,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/html": [
       "<div>\n",
       "<style scoped>\n",
       "    .dataframe tbody tr th:only-of-type {\n",
       "        vertical-align: middle;\n",
       "    }\n",
       "\n",
       "    .dataframe tbody tr th {\n",
       "        vertical-align: top;\n",
       "    }\n",
       "\n",
       "    .dataframe thead th {\n",
       "        text-align: right;\n",
       "    }\n",
       "</style>\n",
       "<table border=\"1\" class=\"dataframe\">\n",
       "  <thead>\n",
       "    <tr style=\"text-align: right;\">\n",
       "      <th></th>\n",
       "      <th>age</th>\n",
       "      <th>workclass</th>\n",
       "      <th>education-num</th>\n",
       "      <th>marital-status</th>\n",
       "      <th>occupation</th>\n",
       "      <th>relationship</th>\n",
       "      <th>race</th>\n",
       "      <th>sex</th>\n",
       "      <th>capital-gain</th>\n",
       "      <th>capital-loss</th>\n",
       "      <th>hours-per-week</th>\n",
       "      <th>native-country</th>\n",
       "      <th>income</th>\n",
       "    </tr>\n",
       "  </thead>\n",
       "  <tbody>\n",
       "    <tr>\n",
       "      <th>0</th>\n",
       "      <td>(16.999, 26.0]</td>\n",
       "      <td>Private</td>\n",
       "      <td>7.0</td>\n",
       "      <td>Never-married</td>\n",
       "      <td>Machine-op-inspct</td>\n",
       "      <td>Own-child</td>\n",
       "      <td>Black</td>\n",
       "      <td>Male</td>\n",
       "      <td>0.0</td>\n",
       "      <td>0.0</td>\n",
       "      <td>FullTime</td>\n",
       "      <td>United-States</td>\n",
       "      <td>0</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>1</th>\n",
       "      <td>(34.0, 41.0]</td>\n",
       "      <td>Private</td>\n",
       "      <td>9.0</td>\n",
       "      <td>Married-civ-spouse</td>\n",
       "      <td>Farming-fishing</td>\n",
       "      <td>Married</td>\n",
       "      <td>White</td>\n",
       "      <td>Male</td>\n",
       "      <td>0.0</td>\n",
       "      <td>0.0</td>\n",
       "      <td>OverTime</td>\n",
       "      <td>United-States</td>\n",
       "      <td>0</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>2</th>\n",
       "      <td>(26.0, 34.0]</td>\n",
       "      <td>Local-gov</td>\n",
       "      <td>12.0</td>\n",
       "      <td>Married-civ-spouse</td>\n",
       "      <td>Protective-serv</td>\n",
       "      <td>Married</td>\n",
       "      <td>White</td>\n",
       "      <td>Male</td>\n",
       "      <td>0.0</td>\n",
       "      <td>0.0</td>\n",
       "      <td>FullTime</td>\n",
       "      <td>United-States</td>\n",
       "      <td>1</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>3</th>\n",
       "      <td>(41.0, 50.0]</td>\n",
       "      <td>Private</td>\n",
       "      <td>10.0</td>\n",
       "      <td>Married-civ-spouse</td>\n",
       "      <td>Machine-op-inspct</td>\n",
       "      <td>Married</td>\n",
       "      <td>Black</td>\n",
       "      <td>Male</td>\n",
       "      <td>7688.0</td>\n",
       "      <td>0.0</td>\n",
       "      <td>FullTime</td>\n",
       "      <td>United-States</td>\n",
       "      <td>1</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>4</th>\n",
       "      <td>(26.0, 34.0]</td>\n",
       "      <td>Private</td>\n",
       "      <td>6.0</td>\n",
       "      <td>Never-married</td>\n",
       "      <td>Other-service</td>\n",
       "      <td>Not-in-family</td>\n",
       "      <td>White</td>\n",
       "      <td>Male</td>\n",
       "      <td>0.0</td>\n",
       "      <td>0.0</td>\n",
       "      <td>MidTime</td>\n",
       "      <td>United-States</td>\n",
       "      <td>0</td>\n",
       "    </tr>\n",
       "  </tbody>\n",
       "</table>\n",
       "</div>"
      ],
      "text/plain": [
       "              age  workclass  education-num      marital-status  \\\n",
       "0  (16.999, 26.0]    Private            7.0       Never-married   \n",
       "1    (34.0, 41.0]    Private            9.0  Married-civ-spouse   \n",
       "2    (26.0, 34.0]  Local-gov           12.0  Married-civ-spouse   \n",
       "3    (41.0, 50.0]    Private           10.0  Married-civ-spouse   \n",
       "4    (26.0, 34.0]    Private            6.0       Never-married   \n",
       "\n",
       "          occupation   relationship   race   sex  capital-gain  capital-loss  \\\n",
       "0  Machine-op-inspct      Own-child  Black  Male           0.0           0.0   \n",
       "1    Farming-fishing        Married  White  Male           0.0           0.0   \n",
       "2    Protective-serv        Married  White  Male           0.0           0.0   \n",
       "3  Machine-op-inspct        Married  Black  Male        7688.0           0.0   \n",
       "4      Other-service  Not-in-family  White  Male           0.0           0.0   \n",
       "\n",
       "  hours-per-week native-country  income  \n",
       "0       FullTime  United-States       0  \n",
       "1       OverTime  United-States       0  \n",
       "2       FullTime  United-States       1  \n",
       "3       FullTime  United-States       1  \n",
       "4        MidTime  United-States       0  "
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    }
   ],
   "source": [
    "# load the adult dataset and perform some simple preprocessing steps\n",
    "# See output for a glimpse of the final dataset's characteristics\n",
    "X, y, sample_weight = fetch_adult()\n",
    "data = clean_dataset(X.assign(income=y), \"adult\")\n",
    "display(data.head())\n",
    "\n",
    "# split into train-test data\n",
    "y = data['income']\n",
    "X = data.drop('income', axis=1)\n",
    "X_train, X_test, y_train, y_test = train_test_split(X, y, train_size=0.7, random_state=random_seed, stratify=y)"
   ]
  },
  {
   "attachments": {},
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Example Model to be used for Auditing\n",
    "\n",
    "We use the train set to train a simple logistic regression model. This will serve as the demonstrative model, which we will then treat as a black box and apply our algorithm.\n",
    "\n",
    "Of course, any model can be used in its place. Our purpose here is not to produce a good model, but to audit the fairness of an existing one."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 4,
   "metadata": {},
   "outputs": [],
   "source": [
    "#### here, we incrementally build the example model. It consists of one preprocessing step,\n",
    "#### which is to turn categorical features into the respective one-hot encodings, and\n",
    "#### a simple scikit-learn logistic regressor.\n",
    "categorical_features = X.select_dtypes(include=[\"object\", \"category\"]).columns.to_list()\n",
    "categorical_features_onehot_transformer = ColumnTransformer(\n",
    "    transformers=[\n",
    "        (\"one-hot-encoder\", OneHotEncoder(), categorical_features)\n",
    "    ],\n",
    "    remainder=\"passthrough\"\n",
    ")\n",
    "model = Pipeline([\n",
    "    (\"one-hot-encoder\", categorical_features_onehot_transformer),\n",
    "    (\"clf\", LogisticRegression(max_iter=1500))\n",
    "])\n",
    "\n",
    "#### train the model\n",
    "model = model.fit(X_train, y_train)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 5,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Accuracy = 85.16%\n"
     ]
    }
   ],
   "source": [
    "# showcase model's accuracy\n",
    "y_pred = model.predict(X_test)\n",
    "print(f\"Accuracy = {(y_test.values == y_pred).sum() / y_test.shape[0]:.2%}\")"
   ]
  },
  {
   "attachments": {},
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# A Practical Example of FACTS\n",
    "\n",
    "The real essence of our work starts here. Specifically, we showcase the generation of candidate subpopulation groups and counterfactuals and the detection of those groups that exhibit the greatest unfairness, with respect to one of several metrics."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Load and Fit FACTS"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 6,
   "metadata": {},
   "outputs": [],
   "source": [
    "# load FACTS framework with:\n",
    "# - the model to be audited\n",
    "# - protected attribute \"sex\" and\n",
    "# - assigning equal, unit weights to all features for cost computation.\n",
    "# - no features forbidden from changing, i.e. user can specify any features that cannot change at all.\n",
    "detector = FACTS(\n",
    "    clf=model,\n",
    "    prot_attr=\"sex\",\n",
    "    feature_weights={f: 1 for f in X.columns},\n",
    "    feats_not_allowed_to_change=[]\n",
    ")"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 7,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Computing candidate subgroups.\n"
     ]
    },
    {
     "name": "stderr",
     "output_type": "stream",
     "text": [
      "100%|██████████████████████████████████████████████████████████████████████████| 1046/1046 [00:00<00:00, 523287.45it/s]"
     ]
    },
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Number of subgroups: 563\n",
      "Computing candidate recourses for all subgroups.\n"
     ]
    },
    {
     "name": "stderr",
     "output_type": "stream",
     "text": [
      "\n",
      "100%|█████████████████████████████████████████████████████████████████████████████| 563/563 [00:00<00:00, 50669.32it/s]"
     ]
    },
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Computing percentages of individuals flipped by each action independently.\n"
     ]
    },
    {
     "name": "stderr",
     "output_type": "stream",
     "text": [
      "\n",
      "100%|████████████████████████████████████████████████████████████████████████████████| 590/590 [00:13<00:00, 43.37it/s]"
     ]
    },
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Computing percentages of individuals flipped by any action with cost up to c, for every c\n"
     ]
    },
    {
     "name": "stderr",
     "output_type": "stream",
     "text": [
      "\n",
      "100%|████████████████████████████████████████████████████████████████████████████████| 416/416 [00:12<00:00, 32.57it/s]\n"
     ]
    }
   ],
   "source": [
    "# generates candidate subpopulation groups for bias and candidate actions\n",
    "detector = detector.fit(X_test)"
   ]
  },
  {
   "attachments": {},
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Detect Groups with Unfairness in Protected Subgroups (using \"Equal Choice for Recourse\" metric)\n",
    "\n",
    "Here we demonstrate the `bias_scan` method of our detector, which ranks subpopulation groups from most to least unfair, with respect to the chosen metric and, of course, the protected attribute.\n",
    "\n",
    "For the purposes of the demo, we use the \"Equal Choice for Recourse\" definition / metric. This posits that the classifier acts fairly for the group in question if the protected subgroups can choose among the same number of sufficiently effective actions to achieve recourse. By sufficiently effective we mean those actions (out of all candidates) which work for at least $100\\phi \\%$ (for some $\\phi \\in [0,1]$) of the subgroup.\n",
    "\n",
    "Given this definition, the respective unfairness *metric* is defined to be the difference in the number of sufficiently effective actions between the two protected subgroups.\n",
    "\n",
    "**Suggestion**: this metric may find utility in scenarios where the aim is to guarantee that protected subgroups have a similar range of options available to them when it comes to making adjustments in order to attain a favorable outcome. For example, when evaluating job candidates, the employer may wish to ensure that applicants from different backgrounds (that currently fail to meet expectations) have an equal array of career / retraining opportunities that may land them the job, so as to ensure diversity in all sectors of the company, which employ individuals with a plethora of roles."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 8,
   "metadata": {},
   "outputs": [],
   "source": [
    "# Detects the top `top_count` most biased groups based on the given metric\n",
    "# available metrics are:\n",
    "# - equal-effectiveness\n",
    "# - equal-choice-for-recourse\n",
    "# - equal-effectiveness-within-budget\n",
    "# - equal-cost-of-effectiveness\n",
    "# - equal-mean-recourse\n",
    "# - fair-tradeoff\n",
    "# a short description for each metric is given below\n",
    "detector.bias_scan(\n",
    "    metric=\"equal-choice-for-recourse\",\n",
    "    phi=0.1,\n",
    "    top_count=3\n",
    ")"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 9,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "If \u001b[1mage = (26.0, 34.0], hours-per-week = FullTime\u001b[0m:\n",
      "\tProtected Subgroup '\u001b[1mFemale\u001b[0m', \u001b[34m10.59%\u001b[39m covered\n",
      "\t\tMake \u001b[1m\u001b[31mage = (41.0, 50.0]\u001b[39m, \u001b[31mhours-per-week = OverTime\u001b[39m\u001b[0m with effectiveness \u001b[32m7.73%\u001b[39m.\n",
      "\t\tMake \u001b[1m\u001b[31mage = (41.0, 50.0]\u001b[39m\u001b[0m with effectiveness \u001b[32m3.98%\u001b[39m.\n",
      "\t\tMake \u001b[1m\u001b[31mage = (34.0, 41.0]\u001b[39m, \u001b[31mhours-per-week = OverTime\u001b[39m\u001b[0m with effectiveness \u001b[32m5.39%\u001b[39m.\n",
      "\t\t\u001b[1mAggregate cost\u001b[0m of the above recourses = \u001b[35m0.00\u001b[39m\n",
      "\tProtected Subgroup '\u001b[1mMale\u001b[0m', \u001b[34m13.78%\u001b[39m covered\n",
      "\t\tMake \u001b[1m\u001b[31mage = (41.0, 50.0]\u001b[39m, \u001b[31mhours-per-week = OverTime\u001b[39m\u001b[0m with effectiveness \u001b[32m19.66%\u001b[39m.\n",
      "\t\tMake \u001b[1m\u001b[31mage = (41.0, 50.0]\u001b[39m\u001b[0m with effectiveness \u001b[32m10.63%\u001b[39m.\n",
      "\t\tMake \u001b[1m\u001b[31mage = (34.0, 41.0]\u001b[39m, \u001b[31mhours-per-week = OverTime\u001b[39m\u001b[0m with effectiveness \u001b[32m13.39%\u001b[39m.\n",
      "\t\t\u001b[1mAggregate cost\u001b[0m of the above recourses = \u001b[35m-3.00\u001b[39m\n",
      "\t\u001b[35mBias against Female with respect to equal-choice-for-recourse. Unfairness score = 3.\u001b[39m\n",
      "If \u001b[1mage = (26.0, 34.0], capital-loss = 0.0, hours-per-week = FullTime\u001b[0m:\n",
      "\tProtected Subgroup '\u001b[1mFemale\u001b[0m', \u001b[34m10.34%\u001b[39m covered\n",
      "\t\tMake \u001b[1m\u001b[31mage = (41.0, 50.0]\u001b[39m, \u001b[31mhours-per-week = OverTime\u001b[39m\u001b[0m with effectiveness \u001b[32m7.67%\u001b[39m.\n",
      "\t\tMake \u001b[1m\u001b[31mage = (41.0, 50.0]\u001b[39m\u001b[0m with effectiveness \u001b[32m4.08%\u001b[39m.\n",
      "\t\tMake \u001b[1m\u001b[31mage = (34.0, 41.0]\u001b[39m, \u001b[31mhours-per-week = OverTime\u001b[39m\u001b[0m with effectiveness \u001b[32m5.28%\u001b[39m.\n",
      "\t\t\u001b[1mAggregate cost\u001b[0m of the above recourses = \u001b[35m0.00\u001b[39m\n",
      "\tProtected Subgroup '\u001b[1mMale\u001b[0m', \u001b[34m13.27%\u001b[39m covered\n",
      "\t\tMake \u001b[1m\u001b[31mage = (41.0, 50.0]\u001b[39m, \u001b[31mhours-per-week = OverTime\u001b[39m\u001b[0m with effectiveness \u001b[32m18.43%\u001b[39m.\n",
      "\t\tMake \u001b[1m\u001b[31mage = (41.0, 50.0]\u001b[39m\u001b[0m with effectiveness \u001b[32m9.27%\u001b[39m.\n",
      "\t\tMake \u001b[1m\u001b[31mage = (34.0, 41.0]\u001b[39m, \u001b[31mhours-per-week = OverTime\u001b[39m\u001b[0m with effectiveness \u001b[32m11.92%\u001b[39m.\n",
      "\t\t\u001b[1mAggregate cost\u001b[0m of the above recourses = \u001b[35m-2.00\u001b[39m\n",
      "\t\u001b[35mBias against Female with respect to equal-choice-for-recourse. Unfairness score = 2.\u001b[39m\n",
      "If \u001b[1mhours-per-week = FullTime, native-country = United-States\u001b[0m:\n",
      "\tProtected Subgroup '\u001b[1mFemale\u001b[0m', \u001b[34m41.66%\u001b[39m covered\n",
      "\t\tMake \u001b[1m\u001b[31mhours-per-week = OverTime\u001b[39m\u001b[0m with effectiveness \u001b[32m2.62%\u001b[39m.\n",
      "\t\tMake \u001b[1m\u001b[31mhours-per-week = BrainDrain\u001b[39m\u001b[0m with effectiveness \u001b[32m1.79%\u001b[39m.\n",
      "\t\t\u001b[1mAggregate cost\u001b[0m of the above recourses = \u001b[35m0.00\u001b[39m\n",
      "\tProtected Subgroup '\u001b[1mMale\u001b[0m', \u001b[34m46.78%\u001b[39m covered\n",
      "\t\tMake \u001b[1m\u001b[31mhours-per-week = OverTime\u001b[39m\u001b[0m with effectiveness \u001b[32m10.08%\u001b[39m.\n",
      "\t\tMake \u001b[1m\u001b[31mhours-per-week = BrainDrain\u001b[39m\u001b[0m with effectiveness \u001b[32m8.70%\u001b[39m.\n",
      "\t\t\u001b[1mAggregate cost\u001b[0m of the above recourses = \u001b[35m-1.00\u001b[39m\n",
      "\t\u001b[35mBias against Female with respect to equal-choice-for-recourse. Unfairness score = 1.\u001b[39m\n"
     ]
    }
   ],
   "source": [
    "# prints the result into a nicely formatted report\n",
    "detector.print_recourse_report(\n",
    "    show_action_costs=False,\n",
    "    show_subgroup_costs=True,\n",
    "    show_unbiased_subgroups=False,\n",
    ")"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Example Output Breakdown"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Let us now disect the above example and the output we see, one step at a time.\n",
    "\n",
    "#### Prelude: $\\phi = 0.1$\n",
    "\n",
    "As we mentioned in the general description of this metric, this is the parameter that determines whether we consider an action sufficiently effective or not. So, here, we consider an action effective if it manages to flip the prediction for at least 10% of the individuals under study, and ineffective otherwise.\n",
    "\n",
    "#### **age = (26.0, 34.0], hours-per-week = FullTime**\n",
    "\n",
    "This is the first (hence, most biased) group. The group description is mostly self-explanatory: everything inside this block concerns all those (affected) individuals that are from 26 (not inclusive) to 34 years old and have a fulltime job. Now, since the output has the same structure for all groups, let us consider this group as an example and further disect the output we see in this block.\n",
    "\n",
    "#### *Protected subgroups 'Male' / 'Female'*\n",
    "\n",
    "We split the population of this group, according to the protected attribute. Hence, we distinguish between males that are 26-34 years old and have a fulltime job and females that are 26-34 years old and have a fulltime job.\n",
    "\n",
    "The \"covered\" percentage reported here in blue signifies that out of all affected females, 10.59% are 26-34 years old and have a fulltime job, while the respective percentage for males is 13.78%.\n",
    "\n",
    "#### *Make age = (41.0, 50.0], hours-per-week = OverTime*\n",
    "\n",
    "This is one of the 3 actions we have tried to apply on the individuals in the current subpopulation group. We report the action, along with its effectiveness and, optionally, the cost; here we omit the action cost because the \"Equal Choice for Recourse\" metric does not take it into account.\n",
    "\n",
    "At this point, let us give a more direct interpretation for the **effectiveness**. In this case, for example, the interpretation could be the following: if all females aged 26-34 with fulltime jobs change their age group to 41-50 years old and their working hours to overtime, then 7.73% of them will actually manage to receive the positive prediction from the model. The rest will still receive the negative prediction.\n",
    "\n",
    "#### *Protected Subgroups' Aggregate Cost*\n",
    "\n",
    "The \"aggregate cost of the above recourses\" message shows how we quantify the *cost of recourse* for all actions in each protected subgroup.\n",
    "\n",
    "This is derived directly from the definition of each metric. Here, for example, we use the \"Equal Choice for Recourse\" metric, which counts the number of effective actions available to each of the protected subgroups. In this group, females have no (sufficiently) effective actions, and as such we say that they gain 0 units. Males have 3 effective actions, so they gain 3 units.\n",
    "\n",
    "Finally, to keep the formalization of having costs everywhere, we rephrase this instead into males having a recourse cost of -3 and females having a recourse cost of 0.\n",
    "\n",
    "As we also mention in the next paragraph, the final bias score of the subgroup is nothing more than the absolute difference of these 2 costs.\n",
    "\n",
    "#### *Bias Deduction / Metric Application*\n",
    "\n",
    "Given the above, one can see that the (same) actions, if applied to females of the subpopulation group, cannot yield more than 10% effectiveness, while in males they achieve up to 19.66%! This is why we argue that, in the terms of bias of recourse, this group exhibits bias against females.\n",
    "\n",
    "This is, of course, with respect to the \"Equal Choice for Recourse\" metric, which posits that the 2 protected subgroups should have the same number of effective actions. Since none of the 3 actions are sufficiently effective for females, and all 3 of them are sufficiently effective for males, we score this group as having a bias measure of $|0 - 3| = 3$."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Example without Bias of Recourse\n",
    "\n",
    "For completeness, we also demonstrate how, for some choices of metrics and parameters, FACTS may fail to find any subpopulation groups that exhibit bias between the protected populations, and thus deduce that in this case there is no recourse related bias."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 10,
   "metadata": {},
   "outputs": [],
   "source": [
    "detector.bias_scan(\n",
    "    metric=\"equal-choice-for-recourse\",\n",
    "    phi=0.7,\n",
    "    top_count=3\n",
    ")"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 11,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "\u001b[1mWith the given parameters, no recourses showing unfairness have been found!\u001b[0m\n"
     ]
    }
   ],
   "source": [
    "# prints the result into a nicely formatted report\n",
    "detector.print_recourse_report(\n",
    "    show_action_costs=False,\n",
    "    show_subgroup_costs=True,\n",
    "    show_unbiased_subgroups=False,\n",
    ")"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Aternative API\n",
    "\n",
    "We also provide a more succinct API in the form of a wrapper function. This is closer in style to the API of existing `aif360` detectors.\n",
    "\n",
    "The previous example could be run equivalently with the following."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 12,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "If \u001b[1mage = (26.0, 34.0], hours-per-week = FullTime\u001b[0m:\n",
      "\tProtected Subgroup '\u001b[1mFemale\u001b[0m', \u001b[34m10.59%\u001b[39m covered\n",
      "\t\tMake \u001b[1m\u001b[31mage = (41.0, 50.0]\u001b[39m, \u001b[31mhours-per-week = OverTime\u001b[39m\u001b[0m with effectiveness \u001b[32m7.73%\u001b[39m and counterfactual cost = 2.0.\n",
      "\t\tMake \u001b[1m\u001b[31mage = (41.0, 50.0]\u001b[39m\u001b[0m with effectiveness \u001b[32m3.98%\u001b[39m and counterfactual cost = 1.0.\n",
      "\t\tMake \u001b[1m\u001b[31mage = (34.0, 41.0]\u001b[39m, \u001b[31mhours-per-week = OverTime\u001b[39m\u001b[0m with effectiveness \u001b[32m5.39%\u001b[39m and counterfactual cost = 2.0.\n",
      "\t\t\u001b[1mAggregate cost\u001b[0m of the above recourses = \u001b[35m0.00\u001b[39m\n",
      "\tProtected Subgroup '\u001b[1mMale\u001b[0m', \u001b[34m13.78%\u001b[39m covered\n",
      "\t\tMake \u001b[1m\u001b[31mage = (41.0, 50.0]\u001b[39m, \u001b[31mhours-per-week = OverTime\u001b[39m\u001b[0m with effectiveness \u001b[32m19.66%\u001b[39m and counterfactual cost = 2.0.\n",
      "\t\tMake \u001b[1m\u001b[31mage = (41.0, 50.0]\u001b[39m\u001b[0m with effectiveness \u001b[32m10.63%\u001b[39m and counterfactual cost = 1.0.\n",
      "\t\tMake \u001b[1m\u001b[31mage = (34.0, 41.0]\u001b[39m, \u001b[31mhours-per-week = OverTime\u001b[39m\u001b[0m with effectiveness \u001b[32m13.39%\u001b[39m and counterfactual cost = 2.0.\n",
      "\t\t\u001b[1mAggregate cost\u001b[0m of the above recourses = \u001b[35m-3.00\u001b[39m\n",
      "\t\u001b[35mBias against Female with respect to equal-choice-for-recourse.. Unfairness score = 3.\u001b[39m\n",
      "If \u001b[1mage = (26.0, 34.0], capital-loss = 0.0, hours-per-week = FullTime\u001b[0m:\n",
      "\tProtected Subgroup '\u001b[1mFemale\u001b[0m', \u001b[34m10.34%\u001b[39m covered\n",
      "\t\tMake \u001b[1m\u001b[31mage = (41.0, 50.0]\u001b[39m, \u001b[31mhours-per-week = OverTime\u001b[39m\u001b[0m with effectiveness \u001b[32m7.67%\u001b[39m and counterfactual cost = 2.0.\n",
      "\t\tMake \u001b[1m\u001b[31mage = (41.0, 50.0]\u001b[39m\u001b[0m with effectiveness \u001b[32m4.08%\u001b[39m and counterfactual cost = 1.0.\n",
      "\t\tMake \u001b[1m\u001b[31mage = (34.0, 41.0]\u001b[39m, \u001b[31mhours-per-week = OverTime\u001b[39m\u001b[0m with effectiveness \u001b[32m5.28%\u001b[39m and counterfactual cost = 2.0.\n",
      "\t\t\u001b[1mAggregate cost\u001b[0m of the above recourses = \u001b[35m0.00\u001b[39m\n",
      "\tProtected Subgroup '\u001b[1mMale\u001b[0m', \u001b[34m13.27%\u001b[39m covered\n",
      "\t\tMake \u001b[1m\u001b[31mage = (41.0, 50.0]\u001b[39m, \u001b[31mhours-per-week = OverTime\u001b[39m\u001b[0m with effectiveness \u001b[32m18.43%\u001b[39m and counterfactual cost = 2.0.\n",
      "\t\tMake \u001b[1m\u001b[31mage = (41.0, 50.0]\u001b[39m\u001b[0m with effectiveness \u001b[32m9.27%\u001b[39m and counterfactual cost = 1.0.\n",
      "\t\tMake \u001b[1m\u001b[31mage = (34.0, 41.0]\u001b[39m, \u001b[31mhours-per-week = OverTime\u001b[39m\u001b[0m with effectiveness \u001b[32m11.92%\u001b[39m and counterfactual cost = 2.0.\n",
      "\t\t\u001b[1mAggregate cost\u001b[0m of the above recourses = \u001b[35m-2.00\u001b[39m\n",
      "\t\u001b[35mBias against Female with respect to equal-choice-for-recourse.. Unfairness score = 2.\u001b[39m\n",
      "If \u001b[1mhours-per-week = FullTime, native-country = United-States\u001b[0m:\n",
      "\tProtected Subgroup '\u001b[1mFemale\u001b[0m', \u001b[34m41.66%\u001b[39m covered\n",
      "\t\tMake \u001b[1m\u001b[31mhours-per-week = OverTime\u001b[39m\u001b[0m with effectiveness \u001b[32m2.62%\u001b[39m and counterfactual cost = 1.0.\n",
      "\t\tMake \u001b[1m\u001b[31mhours-per-week = BrainDrain\u001b[39m\u001b[0m with effectiveness \u001b[32m1.79%\u001b[39m and counterfactual cost = 1.0.\n",
      "\t\t\u001b[1mAggregate cost\u001b[0m of the above recourses = \u001b[35m0.00\u001b[39m\n",
      "\tProtected Subgroup '\u001b[1mMale\u001b[0m', \u001b[34m46.78%\u001b[39m covered\n",
      "\t\tMake \u001b[1m\u001b[31mhours-per-week = OverTime\u001b[39m\u001b[0m with effectiveness \u001b[32m10.08%\u001b[39m and counterfactual cost = 1.0.\n",
      "\t\tMake \u001b[1m\u001b[31mhours-per-week = BrainDrain\u001b[39m\u001b[0m with effectiveness \u001b[32m8.70%\u001b[39m and counterfactual cost = 1.0.\n",
      "\t\t\u001b[1mAggregate cost\u001b[0m of the above recourses = \u001b[35m-1.00\u001b[39m\n",
      "\t\u001b[35mBias against Female with respect to equal-choice-for-recourse.. Unfairness score = 1.\u001b[39m\n"
     ]
    }
   ],
   "source": [
    "most_biased_subgroups = FACTS_bias_scan(\n",
    "    X=X_test,\n",
    "    clf=model,\n",
    "    prot_attr=\"sex\",\n",
    "    feature_weights={f: 1 for f in X.columns},\n",
    "    feats_not_allowed_to_change=[],\n",
    "    metric=\"equal-choice-for-recourse\",\n",
    "    phi=0.1,\n",
    "    top_count=3,\n",
    "    verbose=False, # hides progress bars\n",
    "    print_recourse_report=True,\n",
    "    show_action_costs=True,\n",
    "    show_subgroup_costs=True,\n",
    ")"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 13,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "[({'hours-per-week': 'FullTime', 'native-country': 'United-States'}, 1),\n",
       " ({'age': Interval(26.0, 34.0, closed='right'), 'hours-per-week': 'FullTime'},\n",
       "  3),\n",
       " ({'age': Interval(26.0, 34.0, closed='right'),\n",
       "   'capital-loss': 0.0,\n",
       "   'hours-per-week': 'FullTime'},\n",
       "  2)]"
      ]
     },
     "execution_count": 13,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "most_biased_subgroups"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# Short Description of all Definitions / Metrics of Subgroup Recourse Fairness\n",
    "\n",
    "Here we give a brief description of each of the metrics available in our framework apart from \"Equal Choice for Recourse\".\n",
    "\n",
    "## Equal Effectiveness\n",
    "\n",
    "The classifier is considered to act fairly for a population group if the same proportion of individuals in the protected subgroups can achieve recourse.\n",
    "\n",
    "**Suggestion**: this metric ignores costs altogether and compares only the percentage of males VS females that can cross the model's decision boundary by the same actions. We would use it in applications where the goal is equal impact, in the sense that a change (or a set thereof) affects the same proportion of individuals in the protected subgroups. For example, in a hiring scenario, a similar proportion of males and females are expected to benefit from the same change.\n",
    "\n",
    "## Equal Effectiveness within Budget\n",
    "\n",
    "The classifier is considered to act fairly for a population group if the same proportion of individuals in the protected subgroups can achieve recourse with a cost at most $c$, where $c$ is some user-provided cost budget.\n",
    "\n",
    "**Suggestion**: this metric is similar to the above, but puts a bound on how large the cost of an action can be. Could be used to limit changes with undesirably large cost, e.g., salary changes up to 10K.\n",
    "\n",
    "## Equal Cost of Effectiveness\n",
    "\n",
    "The classifier is considered to act fairly for a population group if the minimum cost required to be sufficiently effective in the protected subgroups is equal. Again, as in \"Equal Choice for Recourse\", by \"sufficiently effective\" we refer to those actions that successfully flip the model's decision for at least $100\\phi \\%$ (for $\\phi \\in [0,1]$) of the subgroup.\n",
    "\n",
    "**Suggestion**: this metric could be useful when an external factor imposes a specific threshold, e.g. in credit risk assessment, a guideline which states that the effort required to be 80% certain that you will have your loan accepted should be the same for males and females.\n",
    "\n",
    "## Equal (Conditional) Mean Recourse\n",
    "\n",
    "This definition extends the notion of *burden* from literature ([reference](https://dl.acm.org/doi/10.1145/3375627.3375812)) to the case where not all individuals may achieve recourse. Omitting some details, given any set of individuals, the **conditional mean recourse cost** is the mean recourse cost among the subset of individuals that can actually achieve recourse, i.e. by at least one of the available actions.\n",
    "\n",
    "Given the above, this definition considers the classifier to act fairly for a population group if the (conditional) mean recourse cost for the protected subgroups is the same.\n",
    "\n",
    "**Suggestion**: this metric compares the mean cost required to achieve recourse for the protected subgroups. It could be useful in a scenario like loan approval, where one needs to ensure that the cost of changes needed to receive the loan are the same for males and females on average.\n",
    "\n",
    "## Fair Effectiveness-Cost Trade-Off\n",
    "\n",
    "This is the strictest definition, which considers the classifier to act fairly for a population group only if the protected subgroups have the same effectiveness-cost distribution (checked in the implementation via a statistical test).\n",
    "\n",
    "Equivalently, Equal Effectiveness within Budget must hold for *every* value of the cost budget $c$.\n",
    "\n",
    "**Suggestion**: this metric considers all available actions and compares all their possible trade-offs between effectiveness and cost among the protected subgroups. This could be useful for cases where the protected attribute should have absolutely no impact on the available options to achieve recourse, such as in high-risk situations like estimating the risk of a convicted individual to act unlawfully in the future (as in the well known [COMPAS dataset](https://www.propublica.org/datastore/dataset/compas-recidivism-risk-score-data-and-analysis))."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": []
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "Python 3 (ipykernel)",
   "language": "python",
   "name": "python3"
  },
  "language_info": {
   "codemirror_mode": {
    "name": "ipython",
    "version": 3
   },
   "file_extension": ".py",
   "mimetype": "text/x-python",
   "name": "python",
   "nbconvert_exporter": "python",
   "pygments_lexer": "ipython3",
   "version": "3.8.18"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 4
}
